/*
 * Copyright 2010-2017 JetBrains s.r.o.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.jetbrains.kotlin.load.java.structure.impl

import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiClass
import com.intellij.psi.PsiElementFactory
import com.intellij.psi.PsiTypeParameter
import com.intellij.psi.SyntaxTraverser
import com.intellij.psi.search.SearchScope
import org.jetbrains.kotlin.asJava.KtLightClassMarker
import org.jetbrains.kotlin.asJava.isSyntheticValuesOrValueOfMethod
import org.jetbrains.kotlin.descriptors.Visibility
import org.jetbrains.kotlin.load.java.structure.*
import org.jetbrains.kotlin.load.java.structure.impl.source.JavaElementPsiSource
import org.jetbrains.kotlin.load.java.structure.impl.source.JavaElementSourceFactory
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.psi.KtPsiUtil

class JavaClassImpl(psiClassSource: JavaElementPsiSource<PsiClass>) : JavaClassifierImpl<PsiClass>(psiClassSource), VirtualFileBoundJavaClass, JavaAnnotationOwnerImpl, JavaModifierListOwnerImpl {

    @SuppressWarnings("unused") // used in KSP
    constructor(psiClass: PsiClass) : this(JavaElementSourceFactory.getInstance(psiClass.project).createPsiSource(psiClass))

    init {
        assert(psiClassSource.psi !is PsiTypeParameter) { "PsiTypeParameter should be wrapped in JavaTypeParameter, not JavaClass: use JavaClassifier.create()" }
    }

    override val innerClassNames: Collection<Name>
        get() = psi.innerClasses.mapNotNull { it.name?.takeIf(Name::isValidIdentifier)?.let(Name::identifier) }

    override fun findInnerClass(name: Name): JavaClass? {
        return psi.findInnerClassByName(name.asString(), false)?.let { JavaClassImpl(psiElementSource.factory.createPsiSource(it)) }
    }

    override val fqName: FqName?
        get() {
            val qualifiedName = psi.qualifiedName
            return if (qualifiedName == null) null else FqName(qualifiedName)
        }

    override val name: Name
        get() = KtPsiUtil.safeName(psi.name)

    override val isInterface: Boolean
        get() = psi.isInterface

    override val isAnnotationType: Boolean
        get() = psi.isAnnotationType

    override val isEnum: Boolean
        get() = psi.isEnum

    override val isSealed: Boolean
        get() = JavaElementUtil.isSealed(this)

    override val permittedTypes: Sequence<JavaClassifierType>
        get() {
            if (!isSealed) return emptySequence()

            val permitsListTypes = psi.permitsListTypes
            return if (permitsListTypes.isNotEmpty()) {
                permitsListTypes.convertIndexed { index, _ ->
                    JavaClassifierTypeImpl(sourceFactory.createPermittedTypeSource(psiElementSource, index))
                }.asSequence()
            } else {
                lazilyComputePermittedTypesInSameFile(psiElementSource)
            }
        }

    override val isRecord: Boolean
        get() = psi.isRecord

    override val outerClass: JavaClassImpl?
        get() {
            val outer = psi.containingClass
            return if (outer == null) null else JavaClassImpl(psiElementSource.factory.createPsiSource(outer))
        }

    override val typeParameters: List<JavaTypeParameter>
        get() = typeParameters(psi.typeParameters, sourceFactory)

    override val supertypes: Collection<JavaClassifierType>
        get() = psi.superTypes.convertIndexed { index, _ ->
            JavaClassifierTypeImpl(sourceFactory.createSuperTypeSource(psiElementSource, index))
        }

    override val methods: Collection<JavaMethod>
        get() {
            assertNotLightClass()
            // We apply distinct here because PsiClass#getMethods() can return duplicate PSI methods, for example in Lombok (see KT-11778)
            // Return type seems to be null for example for the 'clone' Groovy method generated by @AutoClone (see EA-73795)
            return methods(
                psi.methods.filter { method ->
                    !method.isConstructor && method.returnType != null && !(isEnum && isSyntheticValuesOrValueOfMethod(method))
                },
                sourceFactory,
            ).distinct()
        }

    override val fields: Collection<JavaField>
        get() {
            assertNotLightClass()
            return fields(psi.fields.filter {
                // ex. Android plugin generates LightFields for resources started from '.' (.DS_Store file etc)
                Name.isValidIdentifier(it.name)
            }, sourceFactory)
        }

    override val constructors: Collection<JavaConstructor>
        get() {
            assertNotLightClass()
            // See for example org.jetbrains.plugins.scala.lang.psi.light.ScFunctionWrapper,
            // which is present in getConstructors(), but its isConstructor() returns false
            return constructors(psi.constructors.filter { method -> method.isConstructor }, sourceFactory)
        }

    override val recordComponents: Collection<JavaRecordComponent>
        get() {
            assertNotLightClass()

            return psi.recordComponents.convert { JavaRecordComponentImpl(psiElementSource.factory.createPsiSource(it)) }
        }

    override fun hasDefaultConstructor() = !isInterface && constructors.isEmpty()

    override val isAbstract: Boolean
        get() = JavaElementUtil.isAbstract(this)

    override val isStatic: Boolean
        get() = JavaElementUtil.isStatic(this)

    override val isFinal: Boolean
        get() = JavaElementUtil.isFinal(this)

    override val visibility: Visibility
        get() = JavaElementUtil.getVisibility(this)

    override val lightClassOriginKind: LightClassOriginKind?
        get() = (psi as? KtLightClassMarker)?.originKind

    override val virtualFile: VirtualFile?
        get() =  psi.containingFile?.virtualFile

    override fun isFromSourceCodeInScope(scope: SearchScope): Boolean = psi.containingFile.virtualFile in scope

    override fun getAnnotationOwnerPsi() = psi.modifierList

    private fun assertNotLightClass() {
        val psiClass = psi
        if (psiClass !is KtLightClassMarker) return

        val message = "Querying members of JavaClass created for $psiClass of type ${psiClass::class.java} defined in file ${psiClass.containingFile?.virtualFile?.canonicalPath}"
        LOGGER.error(message)
    }

    companion object {
        private val LOGGER = Logger.getInstance(JavaClassImpl::class.java)
    }
}

private fun lazilyComputePermittedTypesInSameFile(psiElementSource: JavaElementPsiSource<PsiClass>): Sequence<JavaClassifierType> {
    return Sequence {
        // Don't capture PSI directly but only via psiElementSource
        val psi = psiElementSource.psi
        val elementFactory = PsiElementFactory.getInstance(psi.project)

        SyntaxTraverser.psiTraverser(psi.containingFile)
            .filter(PsiClass::class.java)
            // isInheritor can lead to resolution which can cause contract violations,
            // that's why we compute it lazily only when the sequence is iterated.
            .filter { it.isInheritor(psi, /* checkDeep: */ false) }
            .map { JavaClassifierTypeImpl(psiElementSource.factory.createTypeSource(elementFactory.createType(it))) }
            .iterator()
    }
}